package cu.ds.logicalsync.fytasrealm.communication;

import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Observable;
import java.util.Set;

import cu.ds.logicalsync.fytasrealm.communication.model.PeerInfo;
import cu.ds.logicalsync.fytasrealm.util.Tools;

import net.jxta.credential.AuthenticationCredential;
import net.jxta.credential.Credential;
import net.jxta.discovery.DiscoveryEvent;
import net.jxta.discovery.DiscoveryListener;
import net.jxta.discovery.DiscoveryService;
import net.jxta.document.Advertisement;
import net.jxta.document.AdvertisementFactory;
import net.jxta.document.StructuredDocument;
import net.jxta.exception.PeerGroupException;
import net.jxta.id.IDFactory;
import net.jxta.membership.Authenticator;
import net.jxta.membership.MembershipService;
import net.jxta.peer.PeerID;
import net.jxta.peergroup.PeerGroup;
import net.jxta.peergroup.PeerGroupID;
import net.jxta.pipe.PipeService;
import net.jxta.platform.Module;
import net.jxta.platform.NetworkManager;
import net.jxta.protocol.DiscoveryResponseMsg;
import net.jxta.protocol.ModuleImplAdvertisement;
import net.jxta.protocol.PeerAdvertisement;
import net.jxta.protocol.PeerGroupAdvertisement;
import net.jxta.protocol.PipeAdvertisement;

/**
 * NetworkService handle the following operations:
 * <ul>
 * 	<li>Join the game network</li>
 * 	<li>Leave the game network</li>
 * 	<li>Check status of other Peers in specific period (default is 10 seconds)</li>
 * 	<li>TODO: Confirm with Eii about other operation required</li>
 * </ul>
 * 
 * @author r.vashira
 * @deprecated
 */
public class NetworkService extends Observable implements DiscoveryListener {
	/** Constants **/
	//TODO: to be constant from Facade
	private static final String FYTAS_REALM_GROUP = "FytasRealm";
	private static final String FYTAS_REALM_GROUP_DESC = "Fyta's Realm Game Network - LogicalSync";
	
	/** Member Variable **/
	/** All PeerGroup over JXTA Network */
	private PeerGroup netPeerGroup = null;
	/** Fyta's Realm PeerGroup */
	private PeerGroup peerGroup = null;
	
	private transient NetworkManager manager = null;
	private transient DiscoveryService discoveryService = null;
	
	private boolean online = false;
	private Set<PeerInfo> peerCollection;
	
	// TODO: Move to BaseService
	private String myPeerName;
	private PeerID myPeerID;
	private PeerGroupID myPeerGroupID;
	
	/** Properties **/
	/**
	 * Is client connect to NetWorkService
	 * @return <code>true</code> if connected, otherwise <code>false</code>
	 */
	public boolean isOnline() {
		return online;
	}
	
	/**
	 * Constructor for NetworkService
	 */
	public NetworkService(String myPeerName) {
		super();
		
		this.myPeerName = myPeerName;
		this.peerCollection = new HashSet<PeerInfo>();
		this.myPeerID = initializeMyPeerID(myPeerName);
		this.myPeerGroupID = initializePeerGroupID(FYTAS_REALM_GROUP);
		
		try {
			//TODO: Parse configuration from xml
			manager = new NetworkManager(NetworkManager.ConfigMode.ADHOC, 
					FYTAS_REALM_GROUP,
					new File(new File(".jxta"), FYTAS_REALM_GROUP).toURI());
			
			manager.setPeerID(myPeerID);
			//Tools.popInformationMsg(FYTAS_REALM_GROUP, "Setting the peer ID:\n\n" + myPeerID.toString());
			
			manager.startNetwork();
			//Tools.popInformationMsg(FYTAS_REALM_GROUP, "Starting the JXTA network");
		} catch (IOException e) {
			e.printStackTrace();
		} catch (PeerGroupException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * TODO: Move to ServiceProvider Interface 
	 */
	public void start() {
		try {
			goOnline();
		} catch (Exception e) {
			//log.error(e);
			e.printStackTrace();
			throw new RuntimeException("Unable to go online");
		}
        
		while(true) {
			run();
		}
	}
	
	/**
	 * TODO: Move to ServiceProvider Interface
	 */
	public void stop() {
		goOffline();
	}

	/* (non-Javadoc)
	 * @see net.jxta.discovery.DiscoveryListener#discoveryEvent(net.jxta.discovery.DiscoveryEvent)
	 */
	public void discoveryEvent(DiscoveryEvent event) {
		DiscoveryResponseMsg res = event.getResponse();
		// IMPORTATANT NOTE: this method will be call ONCE a peer publish advertisement
        System.out.println(" [  Got a Discovery Response [" + res.getResponseCount() + " elements]  from peer : " + event.getSource() + "  ]");
        //Tools.popInformationMsg(FYTAS_REALM_GROUP, "[  Got a Discovery Response [" + res.getResponseCount() + " elements]  from peer : " + event.getSource() + "  ]");
        
        recievePingFromPeer(event);
	}

	/** Helper Method */
	
	/**
	 * CALL THIS ONCE
	 * @param myPeerName
	 * @return
	 */
	private PeerID initializeMyPeerID(String myPeerName) {
		return IDFactory.newPeerID(PeerGroupID.defaultNetPeerGroupID, myPeerName.getBytes());
	}
	
	/**
	 * CALL THIS ONCE
	 * @param myPeerGroupName
	 * @return
	 */
	private PeerGroupID initializePeerGroupID(String myPeerGroupName) {
		return IDFactory.newPeerGroupID(myPeerGroupName.getBytes());
	}
	
	private void run() {
		// TODO: make it configurable
        long waittime = 10 * 1000L;
        long lifetime = 60 * 2 * 1000L;
        long expiration = 60 * 2 * 1000L;
        
        try {
            System.out.println("Sleeping for :" + waittime);
            Thread.sleep(waittime);
        } catch (Exception e) {
            // ignored
        }
        
        // Listen Group Adv
       // discoveryService.getRemoteAdvertisements(null, DiscoveryService.GROUP, "Name", FYTAS_REALM_GROUP, 1, null);
        discoveryService.getRemoteAdvertisements(null, DiscoveryService.PEER, "Name", "PING_TOKEN", 1, null);
        
        // Inform other peer that i'm still alive
    	//PipeAdvertisement pingMsg = createPingMessage();
        PeerAdvertisement pingMsg = createPeerPingMessage();
        
    	 // publish the advertisement with a lifetime of 2 minutes
        System.out.println(
                "Publishing the following advertisement with lifetime :" + lifetime + " expiration :" + expiration);
        System.out.println(pingMsg.toString());
        try {
			discoveryService.publish(pingMsg, lifetime, expiration);
		} catch (IOException e) {
			e.printStackTrace();
		}
        discoveryService.remotePublish(pingMsg, expiration);
        System.out.println("Sending a Discovery Message");
	}
	
	private void recievePingFromPeer(DiscoveryEvent ev) {
		DiscoveryResponseMsg res = ev.getResponse();
    	Advertisement adv;
        Enumeration<Advertisement> en = res.getAdvertisements();

        if (en != null) {
        	System.out.println("In NetworkService.recievePingFromPeer() get an Adv");
            while (en.hasMoreElements()) {
                adv = en.nextElement();
                System.out.println(adv);
                if(adv instanceof PeerAdvertisement) {
            		managePeerCollection((PeerAdvertisement)adv);
            		System.out.println("Found PING_TOKEN");
            	}
            }
        }
		
    	// mark as changed
    	this.setChanged();
    	
    	
    	// Send complere Set of Peer that we know they alive so far to the observer (UI components) 
    	//managePeerCollection(ev);
    	notifyObservers(this.peerCollection);
    }
	
	 private void managePeerCollection(PeerAdvertisement peerAdv) {
		 PeerID sourceId = peerAdv.getPeerID();
		 
		 // TODO: Send clientName as a part of advertisement
		 PeerInfo newPeer = new PeerInfo(sourceId.toString());
		 newPeer.setId(sourceId);

		 boolean addSuccess = this.peerCollection.add(newPeer);
		 if(addSuccess) {
			 System.out.println("Add new Peer to the collection");
		 } else {
			 System.out.println("Peer is already in the collection");
		 }
	 }
	
	 /**
     * Manage Peer collection by add new discovered peer on the list
     * @param ev
     * @deprecated
     */
    private void managePeerCollection(DiscoveryEvent ev) {
    	// TODO: Append self to peer collection?
    	
    	// Create new Peer from DiscoveryEvent's source
    	DiscoveryResponseMsg res = ev.getResponse();
    	PeerAdvertisement peerAdv = res.getPeerAdvertisement();
    	String sourceId = peerAdv.getName();
    	// TODO: Send clientName as a part of advertisement
    	PeerInfo newPeer = new PeerInfo(sourceId);
    	//newPeer.setId(sourceId);
    	
    	boolean addSuccess = this.peerCollection.add(newPeer);
    	if(addSuccess) {
    		System.out.println("Add new Peer to the collection");
    	} else {
    		System.out.println("Peer is already in the collection");
    	}
    }
	
	/** 
	 * 
	 * @throws CommunicationException
	 */
	private void goOnline() throws CommunicationException {
		this.online = false;
		
		// Get the NetPeerGroup
		netPeerGroup = manager.getNetPeerGroup();
		// Create Game Group (Application Group) and also init discoveryService
		peerGroup = createGameGroup(netPeerGroup);
		// Join the Game Group
		joinGroup(peerGroup);

		// TODO: Set Service
		
		this.online = true;
	}
	
	private void goOffline() {
		if (online) {
			peerGroup = null;
			online = false;
		}
		manager.stopNetwork();
	}
	
	// TODO: Move to Utility class
	private PeerGroup createGameGroup(PeerGroup netPeerGroup) {
		PeerGroupAdvertisement adv = null;
		PeerGroup pg = null;
		
		try {
			ModuleImplAdvertisement implAdv =
			    netPeerGroup.getAllPurposePeerGroupImplAdvertisement();
			
			// The creation includes local publishing
			pg = netPeerGroup.newGroup(
					this.myPeerGroupID, 
					implAdv,
					FYTAS_REALM_GROUP,
					FYTAS_REALM_GROUP_DESC);
			//Tools.popInformationMsg(FYTAS_REALM_GROUP, "Creating the new peer group:\n\n" + FYTAS_REALM_GROUP);
			
			// Don't forget to start your peer group
            if (Module.START_OK == pg.startApp(new String[0])) {
            	// TODO: System Log - Create FytaRealm Group in JXTA Network successfully. 
                //Tools.popInformationMsg(FYTAS_REALM_GROUP, "New peer group started successfully");
            } else {
            	// TODO: System Log - FAIL Create FytaRealm Group in JXTA Network.  
                //Tools.popInformationMsg(FYTAS_REALM_GROUP, "New peer group NOT started successfully !!!");
            }
			
			// Initial DiscoveryService
			this.discoveryService = pg.getDiscoveryService();
			// Add ourselves as a listener.
			discoveryService.addDiscoveryListener(this);
			
			// Publish PeerGroupAdvertisement so other client able to join
			adv = pg.getPeerGroupAdvertisement();
			discoveryService.publish(adv);
			discoveryService.remotePublish(adv);
	        //TODO: Logging
	        System.out.println("Group '" + adv.getName() + "' published successfully.");
	        
		} catch (Exception e) {
			e.printStackTrace();
			// Unhandle exception
			throw new RuntimeException(e.getMessage());
		}
		
		return pg;
	}

	// TODO: Move to Utility class
	private void joinGroup(PeerGroup grp) throws CommunicationException {
		StructuredDocument creds = null;
		try {
			// Generate the credentials for the Peer Group
			AuthenticationCredential authCred = new AuthenticationCredential(grp, null, creds);

			// Get the MembershipService from the peer group
			MembershipService membership = grp.getMembershipService();

			// Get the Authenticator from the Authentication creds
			Authenticator auth = membership.apply(authCred);

			// Check if everything is okay to join the group
			if (auth.isReadyForJoin()) {
				Credential myCred = membership.join(auth);
			} else {
				throw new CommunicationException("Unable to join PeerGroup");
			}
			//log.debug("PeerGroup joint");
			System.out.println("PeerGroup joint");
		} catch (Exception e) {
			//log.error(e);
			throw new CommunicationException("Unable to join PeerGroup", e);
		}
	}

	private PipeAdvertisement createPingMessage() {
    	PipeAdvertisement advertisement = 
    		(PipeAdvertisement) AdvertisementFactory.newAdvertisement(PipeAdvertisement.getAdvertisementType());

    	advertisement.setPipeID(IDFactory.newPipeID(peerGroup.getPeerGroupID()));
        advertisement.setType(PipeService.UnicastType);
        advertisement.setName("PeerName:" + peerGroup.getPeerName());
        return advertisement;
    }
	
	private PeerAdvertisement createPeerPingMessage() {
		PeerAdvertisement adv =
			(PeerAdvertisement) AdvertisementFactory.newAdvertisement(PeerAdvertisement.getAdvertisementType());

		adv.setPeerGroupID(myPeerGroupID);
		adv.setPeerID(myPeerID);
		adv.setName("PING_TOKEN");		
		return adv;
	}
}
